Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#2869] feat(core): Add the core logic of authorization #3050

Open
wants to merge 79 commits into
base: main
Choose a base branch
from

Conversation

qqqttt123
Copy link
Contributor

@qqqttt123 qqqttt123 commented Apr 19, 2024

What changes were proposed in this pull request?

Add the core logic of authorization.

Why are the changes needed?

Fix: #2869

Does this PR introduce any user-facing change?

No.

How was this patch tested?

Add new uts.

@qqqttt123 qqqttt123 force-pushed the ISSUE-2869-4 branch 3 times, most recently from 53f1324 to 2574f78 Compare April 19, 2024 16:30
@qqqttt123 qqqttt123 self-assigned this Apr 19, 2024
@qqqttt123
Copy link
Contributor Author

@jerryshao This pull request is ready to review.

@qqqttt123
Copy link
Contributor Author

@jerryshao I have removed version filter.

return Utils.ok(
new GroupResponse(
DTOConverters.toDTO(accessControlManager.getGroup(metalake, group))));
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to wrap these two operations into one tree lock?

identifier,
LockType.READ,
() -> {
AuthorizationUtils.checkIsMetalakeOwner(identifier);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the creator only has the permission to do this operation? What if the owner is not available any more?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should have a different role for metalake admins to have the metalake operations.

Copy link
Contributor

@jerryshao jerryshao Apr 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By referring other systems, you should have a series of role and privileges for admin users, like create metalake, access metalake, create/drop role/user/groups. Otherwise, your current solution has lots of limitations.

DTOConverters.toDTO(
accessControlManager.addMetalakeAdmin(request.getName())))));
() -> {
AuthorizationUtils.checkIsServiceAdmin();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you do this in a filter, not just adding it one by one interface?

// Filter the metalakes which aren't created by the user
List<Metalake> filteredMetalakes = Lists.newArrayList();
for (Metalake metalake : orignMetalakes) {
if (metalake.auditInfo().creator().equals(currentUser)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, why only the creator has permission to list metalakes, what if the creator is leaving, how to handle this scenario?

@qqqttt123
Copy link
Contributor Author

@jerryshao Could you help me review this pull request?


@Override
public boolean supportsSecurableObjectType(SecurableObject.Type type) {
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add some comment here andy below for grant privilege?

* object is only used for metalake admin. You can't grant any privilege to this securable object.
* You can't bind this securable object to any role, too.
* object is reserved for system. You can't grant any privilege to this securable object. You
* can't bind this securable object to any role, too.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There you mentioned that "You can give the securable object the privileges CREATE METALAKE", but here you mentioned that "You can't grant any privilege to this securable object". I think these two sentences are self-contradictory, you should have a more accurate description.

Besides, why you can't bind this securable object to a role?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for users, users can't grant any privilege to allMetalakes. But system pre-created a role binds allMetalakes. AllMetalakes isn't used for the normal user.

We shouldn't allow user to use allMetalakes. It can inflluence the privileges of all metalakes.It's too dangerous.

@@ -48,8 +48,18 @@ class UserGroupManager {
}

User addUser(String metalake, String name) throws UserAlreadyExistsException {

AuthorizationUtils.checkMetalakeExists(metalake);
AuthorizationUtils.checkPermission(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a best practice to checkPermission everywhere in this package, why can't you do this in a filter before the request actually executed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation here is so broken and split into two groups of API, which makes the change so hard to maintain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a best practice to checkPermission everywhere in this package, why can't you do this in a filter before the request actually executed?

Some operations will execute some extra actions after the operations are executed For example, list operation. We should filter the entities which we don't have any privilege.
For example, create operation. We should create some system roles for these operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's due to your design model problem, you're not implementing a consistent role base model, which makes the code so broken, in which you differentiate service user, admin user and normal user, each user has a different implementation logic to handle.

Anyway, I don't like the current implementation, it is not a good code implementation.

}
}

public static boolean isNotAllowedToList(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method name is so weird.

}
}

public static boolean isNotAllowedForAnyPrivileges(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to name the method to "isNotXXX", it's no so intuitive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually use !xxxxx. IDEA suggest me use isNotxxx to replace it.

@@ -57,16 +49,195 @@ public static NameIdentifier ofUser(String metalake, String user) {
metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME, Entity.USER_SCHEMA_NAME, user);
}

public static NameIdentifier ofMetalakeAdminRole() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metalake admin is not a role, it is a user with some predefined roles.

@jerqi jerqi force-pushed the ISSUE-2869-4 branch 2 times, most recently from ba4e52a to 33c0244 Compare May 23, 2024 12:54
core/src/main/java/com/datastrato/gravitino/Entity.java Outdated Show resolved Hide resolved
return userEntity;
if (isServiceAdmin(user)) {
if (hasUserMetalakeCreateRole(user)) {
throw new EntityAlreadyExistsException(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you shoud throw EntityAlreadyExistsException, maybe you need need to throw PrivilegesAlreadyGrantedException or something similar.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so.

store.put(userEntity, false /* overwritten */);
return userEntity;
if (isServiceAdmin(user)) {
if (hasUserMetalakeCreateRole(user)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasCreateUserMetalakeRole may be more clear.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to hasCreateMetalakeRole

}
}
}
} catch (NoSuchUserException noSuchUserException) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When will Gravitino throw this kind of exception?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add some comments for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, Maybe we can narrow the try/catch block by the following code.

List<RoleEntity> roles;
try {
      roles = accessControlManager.listRolesByUser(metalake, currentUser);
} catch( XXXX) {
    return false;
}
...
doCheckPermissin()
...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may need to add the following comments.

  1. Any privilege is satisfied for the object privileges to return true.
  2. We only check the allowed privileges and only the user have the allow privileges can do some operations, we will not check if a user has the denied privileges.

Comment on lines 137 to 145
for (RoleEntity role : roles) {
for (Privilege privilege : object.privileges()) {
if (role.hasPrivilegeWithCondition(object, privilege.name(), Privilege.Condition.DENY)) {
continue;
}

if (role.hasPrivilegeWithCondition(object, privilege.name(), Privilege.Condition.ALLOW)) {
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this block, assuming a role has this privileges: "deny on metalake about create table" and the SecurableObject is "deny on a schema of the metalake about create table", In my opinion, satisfyPrivileges should return true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add some comments for this.

core/src/main/java/com/datastrato/gravitino/Entity.java Outdated Show resolved Hide resolved
*/
String SYSTEM_METALAKE_MANAGE_USER_ROLE = "system_role_metalake_system_manage_user";

/** The reserved the prefix of the role names */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reserved role name's prefix.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to The reserved role names' prefix.

}
}
} catch (NoSuchUserException noSuchUserException) {
// If one user doesn't exist in the metalake, the user doesn't have any privilege.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is ok.

continue;
}

if (role.hasPrivilegeWithCondition(object, privilege.name(), Privilege.Condition.ALLOW)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the following case:
Privileges of object is: Create table: allow, create database: allow
Privileges of current user: create table: allow, create table: deny

So the final result is true????

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

try {
store.put(userEntity, false /* overwritten */);
return userEntity;
if (isServiceAdmin(user)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the user is a service admin, do we still need to add roles SYSTEM_METALAKE_MANAGE_USER_ROLE and METALAKE_CREATE_ROLE to it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because we used the role to execute authorization operation.

Copy link
Contributor

@jerryshao jerryshao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did a cursory review, I found that you have many changes in authorization package, but looks like you doesn't have many UTs to cover all the changes. I would suggest you add more tests to cover the changes you made.

String SYSTEM_METALAKE_MANAGE_USER_ROLE = "system_role_metalake_system_manage_user";

/** The reserved the prefix of the role names */
String SYSTEM_RESERVED_ROLE_NAME_PREFIX = "system_role";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would better to move these definitions to Role.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved to Role.

NameIdentifier systemMetalake = NameIdentifier.of(Entity.SYSTEM_METALAKE_RESERVED_NAME);
if (!metalakeManager.metalakeExists(systemMetalake)) {
metalakeManager.createMetalake(systemMetalake, null, Collections.emptyMap());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd better have a new method to do the system-level metalake, catalog and other creations.

Copy link
Contributor

@jerqi jerqi Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because AccessManager need system metalake. I extract a method in this class.
For system catalog and other creation, I can create a new class to handle them.

@@ -294,6 +286,17 @@ public boolean deleteRole(String metalake, String role) throws NoSuchMetalakeExc
return doWithNonAdminLock(() -> roleManager.deleteRole(metalake, role));
}

public List<RoleEntity> getRolesByUserFromMetalake(String metalake, String currentUser) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listRolesByUser

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed.

return true;
} else {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should clearly define the behavior of false. in Gravitino, false only means not existed, here is it suitable to return false?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the role which creates the metalake doesn't exist, we return will false.

return userEntity;
if (isServiceAdmin(user)) {
if (hasUserMetalakeCreateRole(user)) {
throw new EntityAlreadyExistsException(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so.

LOG.warn("User {} in the metalake admin already exists", user, e);
throw new UserAlreadyExistsException("User %s in the metalake admin already exists", user);
LOG.warn("The metalake admin {} has been added before", user, e);
throw new RoleAlreadyExistsException("The metalake admin %s has been added before", user);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you throw "EntityAlreadyExistsException" and convert to "RoleAlreadyExistsException", why don't you directly throw "RoleAlreadyExistsException"?

return AuthorizationUtils.ofUser(Entity.SYSTEM_METALAKE_RESERVED_NAME, user);
}

private boolean hasCreateMetalakeRole(String user) throws IOException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasMetakeCreateRole?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed.

Entity.EntityType.USER);

for (UserEntity userEntity : userEntities) {
// Case 1: If the user is the service admin, we should the system metalake manage user role
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"we should" what? Please fix the grammar issue.

privileges.add(Privileges.RemoveUser.allow());
}

return SecurableObjects.ofMetalake(Entity.SYSTEM_METALAKE_RESERVED_NAME, privileges);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it uses "system" metalake name, not "*"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we need to create a system metalake object with user privilege to check the permission. Please explain more here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add the comment

System metalake will store the privileges which don't belong any concrete metalake. `*` means all the metalakes.  We don't have a concrete metalake named `*`. So we use system metlake to store the privileges about `*`.

@@ -155,6 +162,23 @@ public Namespace namespace() {
return namespace;
}

public boolean hasPrivilegeWithCondition(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest not put privilege checking logic in this object class, you'd create a new method class to implement such logic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the logic to AuthorizationUtils.

@jerqi
Copy link
Contributor

jerqi commented Jun 26, 2024

I just did a cursory review, I found that you have many changes in authorization package, but looks like you doesn't have many UTs to cover all the changes. I would suggest you add more tests to cover the changes you made.

I can add some new uts for AuthorizationUtils.satisfyPrivileges. In my origin thought, I want to cover the cases in the uts and ITS of the filter.

@@ -76,7 +93,16 @@ User addMetalakeAdmin(String user) {

boolean removeMetalakeAdmin(String user) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeUserFromMetalakeAdmin?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good name.

return store.delete(ofMetalakeAdmin(user), Entity.EntityType.USER);
}

if (hasMetalakeCreateRole(user)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if the code goes here, the user is the service admin. Am I right? Why do we need to judge whether the user has the role MetalakeCreateRole?

Copy link
Contributor

@jerqi jerqi Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I return the correct value whether the MetalakeCreateRole is removed successfully.


for (UserEntity userEntity : userEntities) {
// Case 1: If the user is the service admin, we should add the system metalake manage user
// role
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Format the comments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}
}
} catch (NoSuchUserException noSuchUserException) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, Maybe we can narrow the try/catch block by the following code.

List<RoleEntity> roles;
try {
      roles = accessControlManager.listRolesByUser(metalake, currentUser);
} catch( XXXX) {
    return false;
}
...
doCheckPermissin()
...

}
}
}
} catch (NoSuchUserException noSuchUserException) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may need to add the following comments.

  1. Any privilege is satisfied for the object privileges to return true.
  2. We only check the allowed privileges and only the user have the allow privileges can do some operations, we will not check if a user has the denied privileges.

@qqqttt123 qqqttt123 changed the title [#2869] feat(server): Add the authorization for admins [#2869] feat(server): Add the PermissionChcker Jun 27, 2024
@qqqttt123 qqqttt123 changed the title [#2869] feat(server): Add the PermissionChcker [#2869] feat(core): Add the PermissionChcker Jun 27, 2024
@qqqttt123 qqqttt123 changed the title [#2869] feat(core): Add the PermissionChcker [#2869] feat(core): Add the core logic of authorization Jun 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Subtask] Add the core logic of authorization
4 participants